package org.carabiner.infinitest.depgraph;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;

/**
 * The <code>ClassFileParser</code> class is responsible for parsing a Java class file to create a
 * <code>JavaClass</code> instance.
 * 
 * @author <b>Mike Clark</b>
 * @author Clarkware Consulting, Inc.
 */

class ClassFileParser extends AbstractParser
{

  public static final int JAVA_MAGIC = 0xCAFEBABE;
  public static final int CONSTANT_UTF8 = 1;
  public static final int CONSTANT_UNICODE = 2;
  public static final int CONSTANT_INTEGER = 3;
  public static final int CONSTANT_FLOAT = 4;
  public static final int CONSTANT_LONG = 5;
  public static final int CONSTANT_DOUBLE = 6;
  public static final int CONSTANT_CLASS = 7;
  public static final int CONSTANT_STRING = 8;
  public static final int CONSTANT_FIELD = 9;
  public static final int CONSTANT_METHOD = 10;
  public static final int CONSTANT_INTERFACEMETHOD = 11;
  public static final int CONSTANT_NAMEANDTYPE = 12;
  public static final char CLASS_DESCRIPTOR = 'L';
  public static final int ACC_INTERFACE = 0x200;
  public static final int ACC_ABSTRACT = 0x400;

  private String fileName;
  private boolean isAbstract;
  private JavaClass jClass;
  private Constant[] constantPool;
  private DataInputStream in;

  public ClassFileParser()
  {
    reset();
  }

  private void reset()
  {
    isAbstract = false;

    jClass = null;
    constantPool = new Constant[1];
  }

  public JavaClass parse(InputStream is) throws IOException
  {

    reset();

    jClass = new JavaClass("Unknown");

    in = new DataInputStream(is);

    parseMagic();
    parseMinorVersion();
    parseMajorVersion();

    constantPool = parseConstantPool();

    parseAccessFlags();
    parseClassName();
    parseSuperClassName();
    parseInterfaces();
    parseFields();
    parseMethods();
    parseAttributes();
    addClassConstantReferences();

    return jClass;
  }

  private int parseMagic() throws IOException
  {
    int magic = in.readInt();
    if (magic != JAVA_MAGIC)
    {
      throw new IOException("Invalid class file: " + fileName);
    }

    return magic;
  }

  private int parseMinorVersion() throws IOException
  {
    return in.readUnsignedShort();
  }

  private int parseMajorVersion() throws IOException
  {
    return in.readUnsignedShort();
  }

  private Constant[] parseConstantPool() throws IOException
  {
    int constantPoolSize = in.readUnsignedShort();

    Constant[] pool = new Constant[constantPoolSize];

    for (int i = 1; i < constantPoolSize; i++)
    {

      Constant constant = parseNextConstant();

      pool[i] = constant;

      //
      // 8-byte constants use two constant pool entries
      //
      if (constant.getTag() == CONSTANT_DOUBLE || constant.getTag() == CONSTANT_LONG)
      {
        i++;
      }
    }

    return pool;
  }

  private void parseAccessFlags() throws IOException
  {
    int accessFlags = in.readUnsignedShort();

    boolean isAbstract = ((accessFlags & ACC_ABSTRACT) != 0);
    boolean isInterface = ((accessFlags & ACC_INTERFACE) != 0);

    this.isAbstract = isAbstract || isInterface;
    jClass.isAbstract(this.isAbstract);
  }

  private String parseClassName() throws IOException
  {
    int entryIndex = in.readUnsignedShort();
    String className = getClassConstantName(entryIndex);
    jClass.setName(className);
    return className;
  }

  private String parseSuperClassName() throws IOException
  {
    int entryIndex = in.readUnsignedShort();
    String superClassName = getClassConstantName(entryIndex);
    addImport(getPackageName(superClassName));

    return superClassName;
  }

  private String[] parseInterfaces() throws IOException
  {
    int interfacesCount = in.readUnsignedShort();
    String[] interfaceNames = new String[interfacesCount];
    for (int i = 0; i < interfacesCount; i++)
    {
      int entryIndex = in.readUnsignedShort();
      interfaceNames[i] = getClassConstantName(entryIndex);
      addImport(getPackageName(interfaceNames[i]));
    }

    return interfaceNames;
  }

  private FieldOrMethodInfo[] parseFields() throws IOException
  {
    int fieldsCount = in.readUnsignedShort();
    FieldOrMethodInfo[] fields = new FieldOrMethodInfo[fieldsCount];
    for (int i = 0; i < fieldsCount; i++)
    {
      fields[i] = parseFieldOrMethodInfo();
      String descriptor = toUTF8(fields[i].getDescriptorIndex());
      String[] types = descriptorToTypes(descriptor);
      for (int t = 0; t < types.length; t++)
      {
        addImport(getPackageName(types[t]));
      }
    }

    return fields;
  }

  private FieldOrMethodInfo[] parseMethods() throws IOException
  {
    int methodsCount = in.readUnsignedShort();
    FieldOrMethodInfo[] methods = new FieldOrMethodInfo[methodsCount];
    for (int i = 0; i < methodsCount; i++)
    {
      methods[i] = parseFieldOrMethodInfo();
      String descriptor = toUTF8(methods[i].getDescriptorIndex());
      String[] types = descriptorToTypes(descriptor);
      for (int t = 0; t < types.length; t++)
      {
        if (types[t].length() > 0)
        {
          addImport(getPackageName(types[t]));
        }
      }
    }

    return methods;
  }

  private Constant parseNextConstant() throws IOException
  {

    Constant result;

    byte tag = in.readByte();

    switch (tag)
    {

    case (ClassFileParser.CONSTANT_CLASS):
    case (ClassFileParser.CONSTANT_STRING):
      result = new Constant(tag, in.readUnsignedShort());
      break;
    case (ClassFileParser.CONSTANT_FIELD):
    case (ClassFileParser.CONSTANT_METHOD):
    case (ClassFileParser.CONSTANT_INTERFACEMETHOD):
    case (ClassFileParser.CONSTANT_NAMEANDTYPE):
      result = new Constant(tag, in.readUnsignedShort(), in.readUnsignedShort());
      break;
    case (ClassFileParser.CONSTANT_INTEGER):
      result = new Constant(tag, new Integer(in.readInt()));
      break;
    case (ClassFileParser.CONSTANT_FLOAT):
      result = new Constant(tag, new Float(in.readFloat()));
      break;
    case (ClassFileParser.CONSTANT_LONG):
      result = new Constant(tag, new Long(in.readLong()));
      break;
    case (ClassFileParser.CONSTANT_DOUBLE):
      result = new Constant(tag, new Double(in.readDouble()));
      break;
    case (ClassFileParser.CONSTANT_UTF8):
      result = new Constant(tag, in.readUTF());
      break;
    default:
      throw new IOException("Unknown constant: " + tag);
    }

    return result;
  }

  private FieldOrMethodInfo parseFieldOrMethodInfo() throws IOException
  {

    FieldOrMethodInfo result = new FieldOrMethodInfo(in.readUnsignedShort(), in.readUnsignedShort(), in.readUnsignedShort());

    int attributesCount = in.readUnsignedShort();
    for (int a = 0; a < attributesCount; a++)
    {
      parseAttribute();
    }

    return result;
  }

  private void parseAttributes() throws IOException
  {
    int attributesCount = in.readUnsignedShort();
    for (int i = 0; i < attributesCount; i++)
    {
      parseAttribute();
    }
  }

  private void parseAttribute() throws IOException
  {
    int nameIndex = in.readUnsignedShort();

    int attributeLength = in.readInt();
    byte[] value = new byte[attributeLength];
    for (int b = 0; b < attributeLength; b++)
    {
      value[b] = in.readByte();
    }
  }

  private Constant getConstantPoolEntry(int entryIndex) throws IOException
  {

    if (entryIndex < 0 || entryIndex >= constantPool.length)
    {
      throw new IOException("Illegal constant pool index : " + entryIndex);
    }

    return constantPool[entryIndex];
  }

  private void addClassConstantReferences() throws IOException
  {
    for (int j = 1; j < constantPool.length; j++)
    {
      if (constantPool[j].getTag() == CONSTANT_CLASS)
      {
        String name = toUTF8(constantPool[j].getNameIndex());
        addImport(name);
      }

      if (constantPool[j].getTag() == CONSTANT_DOUBLE || constantPool[j].getTag() == CONSTANT_LONG)
      {
        j++;
      }
    }
  }

  private String getClassConstantName(int entryIndex) throws IOException
  {

    Constant entry = getConstantPoolEntry(entryIndex);
    if (entry == null)
    {
      return "";
    }
    return slashesToDots(toUTF8(entry.getNameIndex()));
  }

  private String toUTF8(int entryIndex) throws IOException
  {
    Constant entry = getConstantPoolEntry(entryIndex);
    if (entry.getTag() == CONSTANT_UTF8)
    {
      return (String) entry.getValue();
    }

    throw new IOException("Constant pool entry is not a UTF8 type: " + entryIndex);
  }

  private void addImport(String importClass)
  {
    if (importClass != null && !importClass.contains("."))
    {
      importClass = importClass.replace('/', '.');
      jClass.addImport(importClass);
    }
  }

  private String slashesToDots(String s)
  {
    return s.replace('/', '.');
  }

  private String getPackageName(String s)
  {
    if ((s.length() > 0) && (s.charAt(0) == '['))
    {
      String types[] = descriptorToTypes(s);
      if (types.length == 0)
      {
        return null; // primitives
      }

      s = types[0];
    }

    s = slashesToDots(s);
    int index = s.lastIndexOf(".");
    if (index > 0)
    {
      return s.substring(0, index);
    }

    return "Default";
  }

  private String[] descriptorToTypes(String descriptor)
  {

    int typesCount = 0;
    for (int index = 0; index < descriptor.length(); index++)
    {
      if (descriptor.charAt(index) == ';')
      {
        typesCount++;
      }
    }

    String types[] = new String[typesCount];

    int typeIndex = 0;
    for (int index = 0; index < descriptor.length(); index++)
    {

      int startIndex = descriptor.indexOf(CLASS_DESCRIPTOR, index);
      if (startIndex < 0)
      {
        break;
      }

      index = descriptor.indexOf(';', startIndex + 1);
      types[typeIndex++] = descriptor.substring(startIndex + 1, index);
    }

    return types;
  }

  class Constant
  {

    private byte _tag;

    private int _nameIndex;

    private int _typeIndex;

    private Object _value;

    Constant(byte tag, int nameIndex)
    {
      this(tag, nameIndex, -1);
    }

    Constant(byte tag, Object value)
    {
      this(tag, -1, -1);
      _value = value;
    }

    Constant(byte tag, int nameIndex, int typeIndex)
    {
      _tag = tag;
      _nameIndex = nameIndex;
      _typeIndex = typeIndex;
      _value = null;
    }

    byte getTag()
    {
      return _tag;
    }

    int getNameIndex()
    {
      return _nameIndex;
    }

    Object getValue()
    {
      return _value;
    }
  }

  class FieldOrMethodInfo
  {

    private int _accessFlags;

    private int _nameIndex;

    private int _descriptorIndex;

    FieldOrMethodInfo(int accessFlags, int nameIndex, int descriptorIndex)
    {

      _accessFlags = accessFlags;
      _nameIndex = nameIndex;
      _descriptorIndex = descriptorIndex;
    }

    int getDescriptorIndex()
    {
      return _descriptorIndex;
    }
  }
}
